package simplescenegraph;

import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.text.DecimalFormat;
import java.util.Hashtable;
import javax.swing.JFrame;

public abstract class SimpleSceneGraph implements Runnable {
	
	private static final long ONE_SECOND_IN_NANOSECONDS = 1000000000L;
	
	private static final int NO_DELAYS_PER_YIELD = 16;
	
	private static int MAX_RENDER_SKIPS = 5; // was 2;
	
	protected JFrame frame;
	
	protected long targetTime;
	
	protected long lastDiff = ONE_SECOND_IN_NANOSECONDS;
	
	protected InputListener inputListener;
	
	protected Renderer renderer = null;
	
	protected GroupNode root;
	
	protected Thread thread = null;
	
	protected Hashtable<String, View> viewMap = new Hashtable<String, View>();
	
	protected View currentView = null;
	
	protected volatile boolean isRunning = false;
	
	public static class InputListener implements KeyListener {
		
		static final int KEY_BUFFER_SIZE = 256;
		
		private boolean[] keyBuffer = new boolean[InputListener.KEY_BUFFER_SIZE];
		
		private boolean[] keyPressedBuffer = new boolean[KEY_BUFFER_SIZE];
		
		private boolean[] keyReleasedBuffer = new boolean[KEY_BUFFER_SIZE];
		
		public void keyPressed(KeyEvent e) {
			keyPressedBuffer[e.getKeyCode()] = true;
		}
		
		public void keyReleased(KeyEvent e) {
			keyReleasedBuffer[e.getKeyCode()] = true;
		}
		
		public void keyTyped(KeyEvent e) {
		}
		
		public void reset() {
			for (int i = 0; i < KEY_BUFFER_SIZE; i++) {
				keyBuffer[i] = (keyPressedBuffer[i] && !keyReleasedBuffer[i]);
				keyPressedBuffer[i] = false;
				keyReleasedBuffer[i] = false;
			}
		}
		
		public boolean getKey(int keyCode) {
			return keyBuffer[keyCode];
		}
		
	}
	
	private class FpsCounterDisplayer implements RenderingCallback {
		
		private DecimalFormat formatter = new DecimalFormat("0.0###");
		
		@Override
		public void afterSceneRendering(Renderer renderer) {
			Dimension frameSize = frame.getSize();
			double fps = ONE_SECOND_IN_NANOSECONDS / (double) lastDiff;
			
			renderer.draw("fps: " + formatter.format(fps), frameSize.width - 200, frameSize.height - 60);
		}
		
		@Override
		public void beforeSceneRendering(Renderer renderer) {
		}
		
	}
	
	public SimpleSceneGraph(JFrame frame) {
		this.frame = frame;
		inputListener = new InputListener();
		this.frame.addKeyListener(inputListener);
		root = new GroupNode();
	}
	
	public final void start(int fps) {
		// Time is measured in nanoseconds
		targetTime = (ONE_SECOND_IN_NANOSECONDS / fps);
		
		// TODO: create renderer choice parameter
		renderer = new GLRenderer();
		
		renderer.register(frame);
		
		renderer.addCallback(new FpsCounterDisplayer());
		
		createScene();
		
		thread = new Thread(this);
		thread.start();
	}
	
	public abstract void createScene();
	
	public abstract void processInput();
	
	public final void run() {
		// timing-related variables
		long start, end, diff, sleepTime, overSleep = 0L, excess = 0L;
		int noDelays = 0;
		
		renderer.initialize();
		
		isRunning = true;
		
		start = System.nanoTime();
		while (isRunning) {
			processInput();
			update(lastDiff / ONE_SECOND_IN_NANOSECONDS);
			updateInternal();
			
			if (currentView != null) {
				render();
			}
			
			inputListener.reset();
			
			end = System.nanoTime();
			diff = end - start;
			lastDiff = diff;
			sleepTime = (targetTime - diff) - overSleep;
			
			if (sleepTime > 0) {
				try {
					Thread.sleep(sleepTime / ONE_SECOND_IN_NANOSECONDS);
				} catch (InterruptedException e) {
				}
				
				overSleep = (System.nanoTime() - end) - sleepTime;
			} else {
				excess -= sleepTime;
				overSleep = 0L;
				
				if (++noDelays >= NO_DELAYS_PER_YIELD) {
					Thread.yield();
					noDelays = 0;
				}
			}
			
			start = System.nanoTime();
			
			int skips = 0;
			while ((excess > targetTime) && (skips < MAX_RENDER_SKIPS)) {
				excess -= targetTime;
				update(1.0f);
				updateInternal();
				skips++;
			}
		}
		
		renderer.destroy();
	}
	
	protected void updateInternal() {
		root.updateGeometricState(0, true);
	}
	
	protected void render() {
		renderer.render(currentView, root);
	}
	
	public void update(float deltaTime) {
	}
	
	public void registerView(String name, View view) {
		viewMap.put(name, view);
		
		if (currentView == null) {
			currentView = view;
		}
	}
	
	public void unregisterView(String name) {
		View removedView = viewMap.remove(name);
		if (removedView != null && removedView.equals(currentView)) {
			currentView = null;
		}
	}
	
	public void setCurrentView(String name) {
		currentView = viewMap.get(name);
	}
	
	public GroupNode getRoot() {
		return root;
	}
	
}
